Update .claude with 11 changed files (#4483) - #4495
Conversation
…4483) Recreate and refine the three Diátaxis docs for the typed-outcome ledger shared-connection fix, applying architect review feedback. - reference/typed-outcome-ledger-connection-registry-api.md — path-keyed OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<Connection>>>>> registry, apply_pragmas (WAL + busy_timeout=5000 + foreign_keys=ON), with_busy_retry/is_sqlite_busy bounded backoff, unchanged open() surface, lock ordering, error semantics, security notes, regression contract. - concepts/typed-outcome-ledger-shared-connection.md — the why: concurrent independent connections to one ledger file caused the SQLITE_BUSY burst; one connection per file removes it by construction. Goal-ids anonymized. - howto/diagnose-typed-outcome-database-is-locked.md — operator runbook. Architect refinements applied: - status: "design — not yet implemented" (code is pre-fix); explicit spec-first callout that docs + implementation land in the same PR. - Anonymized production goal-ids in the concept doc. - Snippet-drift disclaimer in the reference doc. - Kept retry-constant hedging; noted busy_timeout rusqlite/PRAGMA equivalence. Wired into docs/index.md and mkdocs.yml (Concepts/Reference/How-to nav). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on (#4483) Concurrent OODA cycles across goals plus the outbox startup-recovery path each opened an independent SQLite connection to the same typed-outcome DB. Under WAL, only one writer holds the file lock at a time; the racing first-init + write acquisitions exceeded the 5s per-connection busy_timeout and surfaced "typed outcome persistence failed: database is locked". Route every handle to a typed-outcome DB path through a process-wide, path-keyed shared-connection registry (Arc<Mutex<Connection>>), so all cycles and startup recovery serialize on a single WAL + busy_timeout connection instead of contending on the file-level write lock. Pragmas (foreign_keys, busy_timeout=5s, journal_mode=WAL) are applied unconditionally at connect. Immediate write transactions gain a bounded, fail-visible backoff retry on SQLITE_BUSY/"database is locked" as belt-and-suspenders for cross-process contention; on exhaustion the persistence error is surfaced unchanged (no silent fallback). Startup recovery reuses the same handler, so it now shares the serialized connection and completes. The eprintln! recovery-incomplete log is routed through structured tracing::error! with goal_id + error fields. Public API is unchanged. Adds a concurrent-cycles + startup-recovery regression test asserting zero lock errors and durable persistence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Automatic checkpoint to preserve work in progress. Tests and implementation saved before refactoring phase.
The doc comment claimed a 100ms cap was reached between busy-retry attempts. With BUSY_RETRY_MAX_ATTEMPTS=5 the macro returns before sleeping on attempt 5, so busy_backoff is only invoked for attempts 1..=4, yielding 10/20/40/80ms. Clarify that the 100ms cap is an overflow guard and unreachable at the current max attempts. Addresses the sole recurring (non-blocking) review note on #4483. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rysweet
left a comment
There was a problem hiding this comment.
Step 17b — Comprehensive Code Review
Verdict: ✅ APPROVE / merge-ready (non-blocking observations only)
Reviewed the full diff (src/typed_ooda/ledger.rs, src/ooda_actions/advance_goal/typed_goal_session.rs, new regression test, docs) against issue #4483 and the resolved design decisions D1–D8.
Verification performed (fresh, on cde7b9c9)
| Check | Command | Result |
|---|---|---|
| Regression test | cargo test --test typed_ooda_outcome_lock_regression --locked |
✅ 1 passed (2.88s) |
| Typed-OODA suite | cargo test --lib typed_ooda --locked |
✅ 50 passed; 0 failed |
| Lint gate | cargo clippy --lib --tests --locked |
✅ no warnings |
| Registry routing | grep Connection::open src/typed_ooda/ |
✅ exactly one — inside shared_connection factory (line 88); no bypassing opens remain |
| No stray IO | grep print!/println!/eprintln!/dbg! in changed files |
✅ none; typed_goal_session.rs:151 now routes through tracing::error! with structured goal_id+error |
Strengths
- Correct root-cause fix. The path-keyed
OnceLock<Mutex<HashMap<PathBuf, Arc<Mutex<Connection>>>>>registry (D1/D2) serializes every write per DB file through one WAL +busy_timeoutconnection, eliminating the file-level write-lock race that produceddatabase is lockedon restart bursts. Path-keying correctly preserves per-test isolation. - Fail-visible, never-swallowed errors.
begin_immediate!retries only on genuineSQLITE_BUSY/DatabaseLocked(is_sqlite_busy), bounded at 5 attempts, and surfaces the unchangedpersistence()error on exhaustion — outward behavior identical to the prior.map_err(persistence)?. - Pragmas applied unconditionally at connect (D3):
foreign_keys=ON,busy_timeout=5s,journal_mode=WAL. - Strong regression test. Asserts BOTH the absence of lock errors AND durable read-back of every terminal — so the fix cannot pass by silently dropping writes. 112 concurrent goals + 16 startup-recovery workers reproduce the post-restart burst.
- Public API unchanged (D7):
CapabilityHandler::open/with_engineer_livenesssignatures identical; additive & non-breaking. - Docs (concept/howto/reference) added and wired into
mkdocs.yml.
Non-blocking observations
canonical_keyrelative-path aliasing. For a bare filename ("outcomes.sqlite3") the parent is empty → filtered out → key falls back to the raw relative path. But"./outcomes.sqlite3"has parent".", which canonicalizes to an absolute dir → a different key for the same file, yielding two connections and defeating serialization. Production opens the ledger via a stable absolute path so this is latent, but consider canonicalizing the empty-parent case against CWD to close the gap defensively.- Process-wide poison blast radius. With one shared connection per path, a panic while holding the connection
Mutexpoisons it for the entire process lifetime — every subsequent op for that path then returnsoutcome ledger lock is poisoned. Previously each handler had an isolated connection. Transactions use RAII rollback so the risk is low, but the blast radius grew; considerclear_poison()/recovery or a doc note on the tradeoff. - Registry never evicts (acknowledged by design). Fine for production (bounded set of DB paths). In long-lived test processes entries accumulate, but the footprint is negligible.
busy_backoffshift bound.1u64 << attempt.min(5)permits a shift of 5 whileBUSY_RETRY_MAX_ATTEMPTSonly drivesattempt1..=4; safe (.min(100)caps the value, no overflow) and the corrected doc comment now matches actual behavior (10/20/40/80ms).
Checklist
- Code quality and standards — idiomatic, well-documented, structured logging
- Test coverage adequate — targeted regression + full typed-OODA suite green
- No TODOs, stubs, or swallowed exceptions — retries surface errors unchanged
- No unimplemented functions
- Logic correctness — verified registry routing, retry predicate, pragmas
- Edge case handling — see non-blocking notes #1/#2 for defensive hardening
None of the observations block merge. Recommend merge once pre-commit/coverage CI complete.
Step 17c — Security ReviewVerdict: ✅ PASS (merge-ready) — 1 LOW-severity repo-hygiene finding, no exploitable vulnerabilities. Reviewed commit Checklist results
Finding — LOW: accidental commit of runtime session artifactsThree files were committed that are not part of the feature:
Non-security notes (already tracked)Registry never evicts (bounded by distinct DB paths — trivial), and relative-vs-absolute path aliasing in Conclusion: No new vulnerabilities introduced. The path-keyed shared-connection fix is secure. The only actionable item is removing the accidentally-committed |
Step 17d — Philosophy Guardian ReviewVerdict: ✅ PASS / merge-ready — the code change is strongly philosophy-compliant. One non-blocking repo-hygiene note (stray runtime artifacts). Compliance checklist
Non-blocking observation (repo hygiene — not a code-quality issue)
Not changed (correct/intentional)Retry logic, registry-no-eviction (by design for long-lived paths), public API signatures, and the WAL + 5s Conclusion: Philosophy-compliant. The one note is a trivial artifact cleanup, not a blocker. |
Untrack .claude/.claude-4483-stash/runtime/ session artifacts that bypassed the root-only /.claude/runtime/ ignore rule, and add a recursive .claude/**/runtime/ rule to prevent recurrence. Addresses non-blocking repo-hygiene finding from PR #4495 reviews (Steps 17b/17c/17d). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📊 Coverage Summary
Coverage data from CI run. Test files matching |
…gistry key Quality-audit findings on the shared-connection registry (#4483): - Verify PRAGMA journal_mode=WAL actually engaged. SQLite reports the resulting mode without erroring when it cannot honor WAL (e.g. an unsupported filesystem); the prior code discarded that result and proceeded silently, defeating the concurrency fix's core guarantee. Now surfaces a PersistenceFailed error instead of a silent fallback. - Anchor a parent-less (bare relative) DB path to the current directory in canonical_key so it still canonicalizes to an absolute key, keeping the one-file/one-shared-connection invariant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Concise workflow-generated PR for .claude.
Issue
Closes #4483
Changed files
Diff stat
Behavior
Implemented behavior through these branch commits:
cde7b9c docs: correct busy_backoff backoff-sequence comment
3f69bbd wip: checkpoint after implementation (steps 7-8)
9d59c43 fix(typed-ooda): serialize outcome ledger through one shared connection (#4483)
1463093 docs(typed-ooda): spec-first docs for shared-connection ledger fix (#4483)
Validation
ℹ NODE_OPTIONS=--max-old-space-size=32768 (saved preference). To change: /home/azureuser/.amplihack/config
I'll start by checking the branch and the changes made so far.
● Get branch and changed files (shell)
│ cd /home/azureuser/src/Simard-deploy-4049/worktrees/feat/issue-4483-fix-systemic-typed-outcome-p
│ ersistence-failed-data && git branch --show-current && echo "---CHANGED FILES---" && git
│ --no-pager diff --stat HEAD~5..HEAD 2>/dev/null | tail -30 && echo "---STATUS---" && git
│ --no-pager status --short
└ 26 lines…
Changes are committed. Let me examine the actual fix and the new regression test.
Risk
No high-risk subsystem pattern detected from changed paths.
Checklist
This PR was created as a draft for review before merging.
Step 16b: Outside-In Testing Results
Tested from the PR branch (
feat/issue-4483-fix-systemic-typed-outcome-persistence-failed-data) as a consumer of the typed-outcome ledger would exercise it.Detected toolchain: Rust CLI (
Cargo.tomlat repo root,Cargo.lockpresent, cargo 1.95.0 / rustc 1.95.0). Per the qa-team skill's repo-type detection, Rust CLI repos validate via nativecargo testrather than the gadugi-agentic-test framework.Chosen strategy: Native
cargo testat the library/integration boundary — the same public API (CapabilityHandler,OutboxWorker) that OODA cycles and outbox startup-recovery use — pluscargo clippyas the CI lint gate. This exercises the fix through the real external consumer boundary (concurrent handlers on one DB file), which is exactly where issue #4483 manifested.database is lockedand every terminal is durably persistedcargo test --test typed_ooda_outcome_lock_regression --lockedtest concurrent_cycles_and_startup_recovery_never_lock_the_outcome_ledger ... ok—1 passed; 0 failedin 3.11scargo test --lib typed_ooda --locked50 passed; 0 failed; 0 ignoredin 0.98scargo clippy --lib --tests --lockedFinished dev profile— no warningsFix count during outside-in testing: 0 (all scenarios passed on the first run; no diagnose→fix→retry iterations were required).
PR CI status at time of testing:
MERGEABLE; security/audit/deny/vet/npm-audit/scripts-tests checks passing, withpre-commitandcoveragestill running.Verification notes: No stray
print!/println!/eprintln!/dbg!in the changed files — the formereprintln!attyped_goal_session.rs:151now routes throughtracing::error!. The regression asserts both the absence of lock errors and durable persistence of every terminal, so the fix cannot pass by silently dropping writes.